Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 

113 rindas
3.6 KiB

  1. /**
  2. * 服务端 API 代理 —— 将 /api/** 请求转发到 ThinkJS 后端
  3. *
  4. * 对齐 ai_uniapp_v2 H5 端的 manifest.json devServer.proxy 行为:
  5. * /api/user/info → https://api.jiefuku.com/user/info
  6. *
  7. * 用途:
  8. * 1. 避免浏览器跨域
  9. * 2. 服务端转发自动携带 cookie,保持登录态
  10. *
  11. * 环境变量 NUXT_PUBLIC_API_BASE 可覆盖目标地址(生产环境指向 https://api.aionline.cc)
  12. */
  13. import { proxyRequest, sendWebResponse } from 'h3'
  14. async function parseUpstreamResponse(response) {
  15. const contentType = response.headers.get('content-type') || ''
  16. const text = await response.text()
  17. if (contentType.includes('application/json')) {
  18. return text ? JSON.parse(text) : {}
  19. }
  20. if (!text) return {}
  21. try {
  22. return JSON.parse(text)
  23. } catch {
  24. return text
  25. }
  26. }
  27. export default defineEventHandler(async (event) => {
  28. const config = useRuntimeConfig(event)
  29. const apiBase = String(config.apiServerBase || config.public.apiBase || '').replace(/\/+$/, '')
  30. if (!apiBase) {
  31. throw createError({
  32. statusCode: 500,
  33. statusMessage: 'NUXT_PUBLIC_API_BASE is not configured',
  34. })
  35. }
  36. // 去掉 /api 前缀,得到真实后端路径
  37. let path = event.path.replace(/^\/api/, '') || '/'
  38. if (!path.startsWith('/')) path = '/' + path
  39. const method = event.method
  40. const url = apiBase + path
  41. const headers = {}
  42. // 转发用户 cookie(保持登录态)
  43. const cookie = event.headers.get('cookie')
  44. if (cookie) {
  45. headers.cookie = cookie
  46. }
  47. // 透传客户端的 Content-Type(multipart/form-data 上传需要)
  48. const contentType = event.headers.get('content-type')
  49. if (contentType) {
  50. headers['content-type'] = contentType
  51. }
  52. const isMultipart = Boolean(contentType?.includes('multipart/form-data'))
  53. // 透传签名的 header(nonce / timestr / token)
  54. const clientNonce = event.headers.get('nonce')
  55. const clientTimestr = event.headers.get('timestr')
  56. const clientToken = event.headers.get('token')
  57. const clientAccept = event.headers.get('accept')
  58. if (clientNonce) headers.nonce = clientNonce
  59. if (clientTimestr) headers.timestr = clientTimestr
  60. if (clientToken) headers.token = clientToken
  61. if (clientAccept) headers.accept = clientAccept
  62. const fetchOptions = { method, headers }
  63. // GET: 查询参数
  64. const query = getQuery(event)
  65. const queryStr = new URLSearchParams(query).toString()
  66. const fullUrl = queryStr ? url + '?' + queryStr : url
  67. // Multipart 必须直接转发原始请求流。先 readRawBody() 在部分 Chromium /
  68. // Nitro 组合下会一直等待流结束,导致上传请求既不到后端也不返回。
  69. if (isMultipart) {
  70. return proxyRequest(event, fullUrl, { headers })
  71. }
  72. // POST: 请求体
  73. if (method === 'POST') {
  74. const body = await readBody(event)
  75. if (body && typeof body === 'object') {
  76. fetchOptions.body = new URLSearchParams(body).toString()
  77. }
  78. }
  79. try {
  80. // SSE 必须透传原始 Web Response;$fetch 会等待响应结束并尝试解析,
  81. // 导致浏览器无法逐块收到上游事件。
  82. // Multipart 同样使用原生 fetch,确保二进制请求体和 boundary 原样到达上游。
  83. if ((clientAccept || '').includes('text/event-stream')) {
  84. const upstreamResponse = await fetch(fullUrl, fetchOptions)
  85. return sendWebResponse(event, upstreamResponse)
  86. }
  87. const upstreamResponse = await fetch(fullUrl, fetchOptions)
  88. return await parseUpstreamResponse(upstreamResponse)
  89. } catch (error) {
  90. console.error('[Server Proxy Error]', url, error.message)
  91. return {
  92. code: 1000,
  93. data: null,
  94. msg: '服务器代理请求失败',
  95. }
  96. }
  97. })